# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

load("//rules:rv.bzl", "rv_rule")
load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cc_toolchain")

def _get_assembler(cc_toolchain):
    """Find the path to riscv-unknown-elf-as."""

    # Note: the toolchain config doesn"t appear to have a good way to get
    # access to the assembler.  We should be able to access it via the
    # the compiler, but I had trouble with //hw/ip/otbn/util/otbn_as.py invoking
    # the compiler as assembler.
    return [f for f in cc_toolchain.all_files.to_list() if f.basename.endswith("as")][0]

def _otbn_assemble_sources(ctx):
    """Helper function that, for each source file in the provided context, adds
    an action to the context that invokes the otbn assember (otbn_as.py),
    producing a corresponding object file. Returns a list of all object files
    that will be generated by these actions.
    """
    cc_toolchain = find_cc_toolchain(ctx).cc
    assembler = _get_assembler(cc_toolchain)

    objs = []
    for src in ctx.files.srcs:
        obj = ctx.actions.declare_file(src.basename.replace("." + src.extension, ".o"))
        objs.append(obj)
        ctx.actions.run(
            outputs = [obj],
            inputs = ([src] +
                      cc_toolchain.all_files.to_list() +
                      [ctx.executable._otbn_as]),
            env = {
                "RV32_TOOL_AS": assembler.path,
            },
            arguments = ["-o", obj.path, src.path],
            executable = ctx.executable._otbn_as,
        )

    return objs

def _otbn_library(ctx):
    """Produces a collection of object files, one per source file, that can be
    used as a dependency for otbn binaries."""
    objs = _otbn_assemble_sources(ctx)

    return [
        DefaultInfo(
            files = depset(objs),
            data_runfiles = ctx.runfiles(files = objs),
        ),
    ]

def _otbn_binary(ctx):
    """The build process for otbn resources currently invokes
    `//hw/ip/otbn/util/otbn_{as,ld,...}.py` to build the otbn resource.
    These programs are python scripts which translate otbn special
    instructions into the proper opcode sequences and _then_ invoke the normal
    `rv32-{as,ld,...}` programs to produce the resource.  These "native"
    otbn resources are the `otbn_objs` and `elf` output groups.

    In order to make the otbn resource useful to the main CPU, the
    otbn resource needs to be included as a blob of data that the main
    CPU can dump into the otbn `imem` area and ask otbn to execute it.
    `util/otbn-build.py` does this with some objcopy-fu, emitting
    `foo.rv32embed.o`.  Bazel's `cc_*` rules really want dependency objects
    expressed as archives rather than raw object files, so I've modified
    `otbn-build` to also emit an archive file.

    _Morally_, the otbn resource is a data dependency.  However the
    practical meaning of a `data` dependency in bazel is a file made
    available at runtime, which is not how we're using the otbn resource.
    The closest analog is something like `cc_embed_data`, which is like
    a data dependency that needs to be linked into the main program.
    We achieve by having `otbn_build.py` emit a conventional RV32I library
    that other rules can depend on in their `deps`.
    """
    cc_toolchain = find_cc_toolchain(ctx).cc
    assembler = _get_assembler(cc_toolchain)

    # Run the otbn assembler on source files to produce object (.o) files.
    objs = _otbn_assemble_sources(ctx)

    # Declare output files.
    elf = ctx.actions.declare_file(ctx.attr.name + ".elf")
    rv32embed = ctx.actions.declare_file(ctx.attr.name + ".rv32embed.o")
    archive = ctx.actions.declare_file(ctx.attr.name + ".rv32embed.a")

    deps = [f for dep in ctx.attr.deps for f in dep.files.to_list()]

    # Run the otbn_build.py script to link object files from the sources and
    # dependencies.
    ctx.actions.run(
        outputs = [elf, rv32embed, archive],
        inputs = (objs +
                  deps +
                  cc_toolchain.all_files.to_list() +
                  ctx.files._otbn_data +
                  [ctx.executable._wrapper]),
        env = {
            "RV32_TOOL_AS": assembler.path,
            "RV32_TOOL_AR": cc_toolchain.ar_executable,
            "RV32_TOOL_LD": cc_toolchain.ld_executable,
            "RV32_TOOL_OBJCOPY": cc_toolchain.objcopy_executable,
        },
        arguments = [
            "--app-name={}".format(ctx.attr.name),
            "--archive",
            "--no-assembler",
            "--out-dir={}".format(elf.dirname),
        ] + [obj.path for obj in (objs + deps)],
        executable = ctx.executable._wrapper,
    )

    feature_configuration = cc_common.configure_features(
        ctx = ctx,
        cc_toolchain = cc_toolchain,
        requested_features = ctx.features,
        unsupported_features = ctx.disabled_features,
    )

    outputs = objs + [elf, rv32embed, archive]
    return [
        DefaultInfo(files = depset(outputs), data_runfiles = ctx.runfiles(files = outputs)),
        OutputGroupInfo(
            otbn_objs = depset(objs + deps),
            elf = depset([elf]),
            rv32embed = depset([rv32embed]),
            archive = depset([archive]),
        ),
        # Emit a CcInfo provider so that this rule can be a dependency in other
        # cc_* rules.
        CcInfo(
            linking_context = cc_common.create_linking_context(
                linker_inputs = depset([cc_common.create_linker_input(
                    owner = ctx.label,
                    libraries = depset([cc_common.create_library_to_link(
                        actions = ctx.actions,
                        feature_configuration = feature_configuration,
                        cc_toolchain = cc_toolchain,
                        static_library = archive,
                    )]),
                )]),
            ),
        ),
    ]

def _otbn_sim_test(ctx):
    """This rule is for standalone OTBN unit tests, which are run on the host
    via the OTBN simulator.

    It first generates binaries using the same method as otbn_binary, then runs
    them on the simulator. Tests are expected to count failures in the w0
    register; the test checks that w0=0 to determine if the test passed.
    """
    providers = _otbn_binary(ctx)

    # Extract the output .elf file from the output group.
    elf = providers[1].elf.to_list()[0]

    exp_file = ctx.file.exp

    # Create a simple script that runs the OTBN test wrapper on the .elf file
    # using the provided simulator path.
    sim_test_wrapper = ctx.executable._sim_test_wrapper
    simulator = ctx.executable._simulator
    ctx.actions.write(
        output = ctx.outputs.executable,
        content = "{} {} {} {}".format(sim_test_wrapper.short_path, simulator.short_path, exp_file.short_path, elf.short_path),
    )

    # Runfiles include sources, the .elf file, the simulator and test wrapper
    # themselves, and all the simulator and test wrapper runfiles.
    runfiles = ctx.runfiles(files = (ctx.files.srcs + [elf, exp_file, ctx.executable._simulator, ctx.executable._sim_test_wrapper]))
    runfiles = runfiles.merge(ctx.attr._simulator[DefaultInfo].default_runfiles)
    runfiles = runfiles.merge(ctx.attr._sim_test_wrapper[DefaultInfo].default_runfiles)
    return [
        DefaultInfo(runfiles = runfiles),
        providers[1],
    ]

def _otbn_consttime_test_impl(ctx):
    """This rule checks if a program or subroutine is constant-time.

    There are some limitations to this check; see the Python script's
    documentation for details. In particular, the check may not be able to
    determine that a program runs in constant-time when in fact it does.
    However, if the check passes, the program should always run in constant
    time; that is, the check can produce false negatives but never false
    positives.

    This rule expects one dependency of an otbn_binary or otbn_sim_test type,
    which should provide exactly one `.elf` file.
    """

    # Extract the output .elf file from the output group.
    elf = [f for t in ctx.attr.deps for f in t[OutputGroupInfo].elf.to_list()]
    if len(elf) != 1:
        fail("Expected only one .elf file in dependencies, got: " + str(elf))
    elf = elf[0]

    # Write a very simple script that runs the checker.
    script_content = "{} {} --verbose".format(ctx.executable._checker.short_path, elf.short_path)
    if ctx.attr.subroutine:
        script_content += " --subroutine {}".format(ctx.attr.subroutine)
    if ctx.attr.secrets:
        script_content += " --secrets {}".format(" ".join(ctx.attr.secrets))
    if ctx.attr.initial_constants:
        script_content += " --constants {}".format(" ".join(ctx.attr.initial_constants))
    ctx.actions.write(
        output = ctx.outputs.executable,
        content = script_content,
    )

    # The .elf file must be added to runfiles in order to be visible to the
    # test at runtime. In addition, we need to add all the runfiles from the
    # checker script itself (e.g. the Python runtime and dependencies).
    runfiles = ctx.runfiles(files = [elf])
    runfiles = runfiles.merge(ctx.attr._checker[DefaultInfo].default_runfiles)
    return [DefaultInfo(runfiles = runfiles)]

def _otbn_insn_count_range(ctx):
    """This rule gets min/max possible instruction counts for an OTBN program.
    """

    # Extract the .elf file to check from the dependency list.
    elf = [f for t in ctx.attr.deps for f in t[OutputGroupInfo].elf.to_list()]
    if len(elf) != 1:
        fail("Expected only one .elf file in dependencies, got: " + str(elf))
    elf = elf[0]

    # Command to run the counter script and extract the min/max values.
    out = ctx.actions.declare_file(ctx.attr.name + ".txt")
    ctx.actions.run_shell(
        outputs = [out],
        inputs = [ctx.file._counter, elf],
        command = "{} {} > {}".format(ctx.file._counter.path, elf.path, out.path),
    )

    runfiles = ctx.runfiles(files = ([out]))
    return [DefaultInfo(files = depset([out]), runfiles = runfiles)]

otbn_library = rv_rule(
    implementation = _otbn_library,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        "_cc_toolchain": attr.label(
            default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
        ),
        "_otbn_as": attr.label(
            default = "//hw/ip/otbn/util:otbn_as",
            executable = True,
            cfg = "exec",
        ),
    },
    fragments = ["cpp"],
    toolchains = ["@rules_cc//cc:toolchain_type"],
    incompatible_use_toolchain_transition = True,
)

otbn_binary = rv_rule(
    implementation = _otbn_binary,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        "deps": attr.label_list(providers = [DefaultInfo]),
        "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
        "_otbn_as": attr.label(
            default = "//hw/ip/otbn/util:otbn_as",
            executable = True,
            cfg = "exec",
        ),
        "_otbn_data": attr.label(
            default = "//hw/ip/otbn/data:all_files",
            allow_files = True,
        ),
        "_wrapper": attr.label(
            default = "//util:otbn_build",
            executable = True,
            cfg = "exec",
        ),
    },
    fragments = ["cpp"],
    toolchains = ["@rules_cc//cc:toolchain_type"],
    incompatible_use_toolchain_transition = True,
)

otbn_sim_test = rv_rule(
    implementation = _otbn_sim_test,
    test = True,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        "deps": attr.label_list(providers = [DefaultInfo]),
        "exp": attr.label(allow_single_file = True),
        "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
        "_otbn_as": attr.label(
            default = "//hw/ip/otbn/util:otbn_as",
            executable = True,
            cfg = "exec",
        ),
        "_otbn_data": attr.label(
            default = "//hw/ip/otbn/data:all_files",
            allow_files = True,
        ),
        "_simulator": attr.label(
            default = "//hw/ip/otbn/dv/otbnsim:standalone",
            executable = True,
            cfg = "exec",
        ),
        "_sim_test_wrapper": attr.label(
            default = "//hw/ip/otbn/util:otbn_sim_test",
            executable = True,
            cfg = "exec",
        ),
        "_wrapper": attr.label(
            default = "//util:otbn_build",
            executable = True,
            cfg = "exec",
        ),
    },
    fragments = ["cpp"],
    toolchains = ["@rules_cc//cc:toolchain_type"],
    incompatible_use_toolchain_transition = True,
)

otbn_consttime_test = rule(
    implementation = _otbn_consttime_test_impl,
    test = True,
    attrs = {
        "srcs": attr.label_list(allow_files = True),
        "deps": attr.label_list(providers = [OutputGroupInfo]),
        "subroutine": attr.string(),
        "secrets": attr.string_list(),
        "initial_constants": attr.string_list(),
        "_checker": attr.label(
            default = "//hw/ip/otbn/util:check_const_time",
            executable = True,
            cfg = "exec",
        ),
    },
)

otbn_insn_count_range = rule(
    implementation = _otbn_insn_count_range,
    attrs = {
        "deps": attr.label_list(providers = [OutputGroupInfo]),
        "_counter": attr.label(
            default = "//hw/ip/otbn/util:get_instruction_count_range.py",
            allow_single_file = True,
        ),
    },
)
